﻿namespace Blaze.FileSystems
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Security.Cryptography;
    using Framework;
    using Framework.Logging;
    using UnityEngine.Profiling;
    using Utilities;

    public class FileSystem
    {
        /// <summary>
        /// 获取或设置默认的文件系统。
        /// </summary>
        public static FileSystem Default
        {
            get
            {
                if (mDefault == null)
                    mDefault = new FileSystem();
                return mDefault;
            }
            set { mDefault = value; }
        }

        /// <summary>
        /// 获取文件系统的根路径。
        /// </summary>
        public string Root { get; }

        public FileSystem(string root = null)
        {
            if (root == null)
                root = PathUtility.PersistentDataPath;
            Root = root;
            ensureDirectory(mCategoryObjects);
            ensureDirectory(mCategoryIndexes);
        }

        /// <summary>
        /// 获取一个值，表示指定文件是否存在于文件系统。
        /// </summary>
        public bool Exists(string path)
        {
            var hash = XXHash64.Hash(path);

            IndexEntry indexEntry;
            if (mEntryMap.TryGetValue(hash, out indexEntry))
                return true;

            foreach (var pkg in mPkgFiles.Values)
            {
                if (pkg.Exists(hash))
                    return true;
            }
            return false;
        }

        /// <summary>
        /// 挂载文件系统。
        /// </summary>
        /// <param name="indexName">索引文件名</param>
        public void Mount(string indexName)
        {
            if (string.IsNullOrEmpty(indexName))
                throw new ArgumentException(nameof(indexName));

            if (mMountedMap.ContainsKey(indexName))
                return;

            mLog.Verbose($"开始：加载索引 {indexName}");
            Profiler.BeginSample("Mount " + indexName);
            byte[] bytes;
            using (var stream = open(mCategoryIndexes, indexName))
            {
                if (stream == null)
                    throw new IOException("index file not found:" + indexName);
                bytes = new byte[stream.Length];
                stream.Read(bytes, 0, bytes.Length);
            }
            var hashedPath = IndexFile.GetHashedPath(bytes);
            using (var stream = open(mCategoryObjects, hashedPath))
            {
                if (stream == null)
                    throw new IOException("index ref not found:" + indexName);
                var indexFile = IndexFile.Read(stream);
                mount(indexName, indexFile);
            }
            Profiler.EndSample();
            mLog.Verbose($"结束：加载索引 {indexName}");
        }

        /// <summary>
        /// 打开并获取指定路径下的文件流。
        /// </summary>
        /// <param name="path">文件路径</param>
        /// <returns>打开成功返回流对象，失败则返回null</returns>
        public Stream Open(string path)
        {
            var hash = XXHash64.Hash(path);

            IndexEntry indexEntry;
            if (mEntryMap.TryGetValue(hash, out indexEntry))
            {
                var ret = open(mCategoryObjects, indexEntry.HashedPath);
                traceOpen(indexEntry, ret);
                return ret;
            }

            foreach (var pkg in mPkgFiles.Values)
            {
                var stream = pkg.Read(hash);
                if (stream != null)
                    return stream;
            }
            return null;
        }

        /// <summary>
        /// 取消挂载文件系统。
        /// </summary>
        /// <param name="indexName">索引文件名</param>
        public void Unmount(string indexName)
        {
            IndexFile indexFile;
            if (!mMountedMap.TryGetValue(indexName, out indexFile))
                return;
            Profiler.BeginSample("Unmount " + indexName);
            foreach (var entry in indexFile)
            {
                if (entry.Type == IndexEntryType.Pkg)
                {
                    var pkgFile = mPkgFiles[entry.Path];
                    pkgFile.Close();
                    mPkgFiles.Remove(entry.Path);
                }
                List<Stream> list;
                if (mEntryOpenedStreams.TryGetValue(entry, out list))
                {
                    foreach (var stream in list)
                        stream.Close();
                }
            }
            mMounted.Remove(indexFile);
            mMountedMap.Remove(indexName);
            rebuild();
            Profiler.EndSample();
        }

        /// <summary>
        /// 为文件系统写入一个索引文件。
        /// </summary>
        /// <param name="indexName">索引名称</param>
        /// <param name="input">索引文件的输入流</param>
        /// <param name="overwrite">是否覆盖</param>
        public byte[] WriteIndex(string indexName, Stream input, bool overwrite = false)
        {
            var path = Path.Combine(Root, mPrefix, mCategoryIndexes, indexName);
            if (!overwrite && File.Exists(path))
                throw new IOException("index file already exists:" + path);
            var dir = Path.GetDirectoryName(path);
            if (!Directory.Exists(dir))
                Directory.CreateDirectory(dir);

            var hash = addObject(input);
            File.WriteAllBytes(path, hash);
            return hash;
        }

        /// <summary>
        /// 向文件系统内写入一个对象
        /// </summary>
        public byte[] WriteObject(Stream input)
        {
            return addObject(input);
        }

        private byte[] addObject(Stream input)
        {
            var pos = input.Position;
            var hash = mSha1.ComputeHash(input);
            var path = IndexFile.GetHashedPath(hash);
            if (!exists(mCategoryObjects, path))
            {
                //如果对象的SHA1相同，那么没必要重复写入文件，因为两个文件必定相同
                input.Position = pos;
                write(input, mCategoryObjects, path);
            }
            return hash;
        }

        private void ensureDirectory(string name)
        {
            var fullPath = $"{Root}/{mPrefix}/{name}";
            if (!Directory.Exists(fullPath))
                Directory.CreateDirectory(fullPath);
        }

        private bool exists(string category, string relativePath)
        {
            var filePath = $"{Root}/{mPrefix}/{category}/{relativePath}";
            return File.Exists(filePath);
        }

        private void mount(string name, IndexFile index)
        {
            foreach (var entry in index)
            {
                mEntryMap[entry.Path] = entry;
                if (entry.Type == IndexEntryType.Pkg)
                {
                    var fs = open(mCategoryObjects, entry.HashedPath);
                    if (fs == null)
                        throw new IOException($"pkg '{entry.HashedPath}' in index file '{name}' not exists");
                    var pkgFile = PkgFile.Read(fs);
                    mPkgFiles.Add(entry.Path, pkgFile);
                }
            }
            mMountedMap.Add(name, index);
            mMounted.Add(index);
        }

        private Stream open(string category, string relativePath)
        {
            var filePath = $"{Root}/{mPrefix}/{category}/{relativePath}";
            if (File.Exists(filePath))
                return File.Open(filePath, FileMode.Open);
            var bytes = StreamingAssets.Load($"{mPrefix}/{category}/{relativePath}");
            if (bytes != null)
                return new MemoryStream(bytes, false);
            return null;
        }

        private void rebuild()
        {
            mEntryMap.Clear();
            foreach (var indexFile in mMounted)
            {
                foreach (var entry in indexFile)
                {
                    mEntryMap[entry.Path] = entry;
                }
            }
        }

        private void traceOpen(IndexEntry indexEntry, Stream stream)
        {
            List<Stream> list;
            if (!mEntryOpenedStreams.TryGetValue(indexEntry, out list))
            {
                list = new List<Stream>();
                mEntryOpenedStreams.Add(indexEntry, list);
            }
            list.Add(stream);
        }

        private void write(Stream input, string category, string relativePath)
        {
            var filePath = $"{Root}/{mPrefix}/{category}/{relativePath}";
            var dirPath = Path.GetDirectoryName(filePath);
            if (!Directory.Exists(dirPath))
                Directory.CreateDirectory(dirPath);
            using (var output = File.Open(filePath, FileMode.OpenOrCreate))
            {
                input.CopyTo(output);
            }
        }

        private const string mPrefix = "fs";
        private const string mCategoryIndexes = "indexes";
        private const string mCategoryObjects = "objects";

        private readonly Dictionary<ulong, PkgFile> mPkgFiles = new Dictionary<ulong, PkgFile>();
        private readonly Dictionary<ulong, IndexEntry> mEntryMap = new Dictionary<ulong, IndexEntry>();
        private readonly Dictionary<IndexEntry, List<Stream>> mEntryOpenedStreams = new Dictionary<IndexEntry, List<Stream>>();
        private readonly List<IndexFile> mMounted = new List<IndexFile>();
        private readonly Dictionary<string, IndexFile> mMountedMap = new Dictionary<string, IndexFile>();
        private readonly Dictionary<ulong, int> mCounter = new Dictionary<ulong, int>(256);
        private readonly SHA1 mSha1 = new SHA1Cng();
        private static FileSystem mDefault;
        private static Log mLog = LogManager.GetLogger(typeof(FileSystem));
    }
}